﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Transactions;
using AopAlliance.Aop;
using Flower.Client;
using Flower.Directory.Util;
using Flower.Processing.Advices;
using Flower.Services;
using Flower.Services.Data;
using Flower.Workflow;
using Flower.Processing.Interruptions;
using Spring.Aop;
using Spring.Aop.Framework;
using Spring.Aop.Support;
using Transaction = Flower.Processing.Advices.Transaction;
using Flower.Logging;
using Process = Flower.Services.Data.Process;
using System.ServiceModel;
using Flower.Services.Clients;

namespace Flower.Processing
{
    /// <summary>
    /// A processor running a workflow.
    /// </summary>
    public sealed class Processor : IContext
    {
        [Flags]
        public enum StandardAdvisors
        {
            None = 0,
            Logging = 1,
            StateSaving = 2,
            Transactions = 4,
            Retrying = 16,
            
            All = Logging | StateSaving | Transactions | Retrying
        }

        public class Settings
        {
            public TimeSpan ProcessorTransactionTimeout { get; set; }
            public double MinWaitOnErrorSeconds { get; set; }
            public int MaxRetries { get; set; }
            public bool EmitBreakpoints { get; set; }
            public StandardAdvisors StandardAdvisors { get; set; }

            public Settings()
            {
                StandardAdvisors = StandardAdvisors.All;
            }
        }

        private static readonly Settings DefaultSettings = new Settings 
        {
            ProcessorTransactionTimeout = TimeSpan.FromMinutes(1),
            MinWaitOnErrorSeconds = 30,
            MaxRetries = 3
        };

        private readonly List<IActivity> _activities;
        private readonly IFlowerClient _client;
        private readonly IStateService _stateService;
        private readonly PassThroughStateService _passThroughStateService;

        private readonly string _name;
        private readonly Settings _settings;

        private string _pid;
        private Workflow.Process _state;
        private WorkflowLogAdapter _log;

        /// <summary>
        /// The Flower client.
        /// </summary>
        public IFlowerClient Client { get { return _client; } }

        /// <summary>
        /// The name of the processor in the directory.
        /// </summary>
        public string ProcessorName { get { return _name; } }

        /// <summary>
        /// Processor's settings.
        /// </summary>
        internal Settings CurrentSettings { get { return _settings; } }

        /// <summary>
        /// State of the process.
        /// </summary>
        public Workflow.Process State { get { return _state; } }

        /// <summary>
        /// Activities of the process.
        /// </summary>
        public IList<IActivity> Activities { get { return _activities.AsReadOnly(); } }

        /// <summary>
        /// PID of the process.
        /// </summary>
        public string Pid { get { return _pid; } }

        /// <summary>
        /// The services provider of the process.
        /// </summary>
        public IServicesProvider ServicesProvider { get { return _client; } }

        /// <summary>
        /// The log.
        /// </summary>
        public ILog Log { get { return _log; } }

        /// <summary>
        /// Indicates whether the final activity has been executed.
        /// </summary>
        public bool IsFinished { get { return _state.__ActivityPointer == _activities.Count; } }

        /// <summary>
        /// The activity to be executed next.
        /// </summary>
        public IActivity NextActivity { get { return _activities[_state.__ActivityPointer]; } }

        /// <summary>
        /// Creates a new instance of a processor.
        /// </summary>
        /// <param name="name">The name of the processor in the directory.</param>
        /// <param name="flowerClient">The Flower client to use.</param>
        public Processor(string name, IFlowerClient flowerClient)
            : this(name, flowerClient, null, DefaultSettings)
        {            
        }

        /// <summary>
        /// Creates a new instance of a processor.
        /// </summary>
        /// <param name="name">The name of the processor in the directory.</param>
        /// <param name="flowerClient">The directory client to use.</param>
        /// <param name="stateService">
        /// The state service to use by the processor.
        /// If null, the states are saved directly to the directory.
        /// </param>
        /// <param name="settings">Processor's settings.</param>
        public Processor(string name, IFlowerClient flowerClient, IStateService stateService, Settings settings)
        {
            if (name == null) throw new ArgumentNullException("name");
            if (flowerClient == null) throw new ArgumentNullException("flowerClient");

            _name = name;
            _settings = settings;
            _client = flowerClient;
            _passThroughStateService = new PassThroughStateService(flowerClient);
            _stateService = stateService ?? _passThroughStateService;
            _activities = new List<IActivity>();
        }

        private static readonly NameMatchMethodPointcut DoMethodPointcut =
            new NameMatchMethodPointcut { MappedName = "Do" };
        private static readonly ComposablePointcut RetryPointcut =
            new ComposablePointcut()
                .Intersection(new AttributeMatchingPointcut(typeof(RetryAttribute)))
                .Intersection((IPointcut)DoMethodPointcut);
        private static readonly DefaultPointcutAdvisor ExceptionHandlingAdvisor =
            new DefaultPointcutAdvisor
            {
                Pointcut = DoMethodPointcut,
                Advice = new ExceptionHandling(),
                Order = 30
            };
        private static readonly ComposablePointcut TransactionPointcut =
            new ComposablePointcut()
                .Intersection(new AttributeMatchingPointcut(typeof(TransactionalAttribute)))
                .Intersection((IPointcut)DoMethodPointcut);
        private static readonly DefaultPointcutAdvisor StateSavingAdvisor =
            new DefaultPointcutAdvisor
            {
                Pointcut =
                    new ComposablePointcut()
                        .Intersection(new AttributeMatchingPointcut(typeof(SaveAfterAttribute)))
                        .Intersection((IPointcut)DoMethodPointcut),
                Advice = new StateSaving(),
                Order = 50
            };

        private void SetupAdvisors(IEnumerable<IAdvisor> customAdvisors)
        {
            var loggingAdvisor = new DefaultPointcutAdvisor
            {
                Pointcut = DoMethodPointcut,
                Advice = new Advices.Logging(_log),
                Order = 10
            };
            var retryingAdvisor = new DefaultPointcutAdvisor
            {
                Pointcut = RetryPointcut,
                Advice = new Retrying(_settings.MinWaitOnErrorSeconds, _settings.MaxRetries),
                Order = 20
            };
            var transactionAdvisor = new DefaultPointcutAdvisor
            {
                Pointcut = TransactionPointcut,
                Advice = new Transaction(_settings.ProcessorTransactionTimeout),
                Order = 40
            };

            for (int i = 0; i < _activities.Count; i++)
            {
                var proxyFactory = new ProxyFactory(_activities[i]);

                if (_settings.StandardAdvisors.HasFlag(StandardAdvisors.Logging))
                {
                    proxyFactory.AddAdvisor(loggingAdvisor);
                }
                
                if (_settings.StandardAdvisors.HasFlag(StandardAdvisors.Retrying))
                {
                    proxyFactory.AddAdvisor(retryingAdvisor);
                }

                proxyFactory.AddAdvisor(ExceptionHandlingAdvisor);

                if (_settings.StandardAdvisors.HasFlag(StandardAdvisors.Transactions))
                {
                    proxyFactory.AddAdvisor(transactionAdvisor);
                }

                if (_settings.StandardAdvisors.HasFlag(StandardAdvisors.StateSaving))
                {
                    proxyFactory.AddAdvisor(StateSavingAdvisor);
                }

                if (customAdvisors != null)
                {
                    foreach (IAdvisor customAdvisor in customAdvisors)
                    {
                        proxyFactory.AddAdvisor(customAdvisor);
                    }
                }

                _activities[i] = (IActivity)proxyFactory.GetProxy();
            }
        }

        private IEnumerable<IActivity> Compile(Workflow.Process process)
        {
            var bld = new ProcessBuilder(process, _settings.EmitBreakpoints);

            ((IProcessDefinition)process).DefineProcess(bld);

            bld.CheckEndState();

            return bld.Activities;
        }

        /// <summary>
        /// Initializes a processor to run a process.
        /// </summary>
        /// <param name="pid">PID of the process.</param>
        /// <param name="process">The process definition.</param>
        /// <param name="customAdvisors">The advisors to apply to the activities.</param>
        public void Init(string pid, Workflow.Process process, IEnumerable<IAdvisor> customAdvisors = null)
        {
            if (pid == null) throw new ArgumentNullException("pid");
            if (process == null) throw new ArgumentNullException("process");

            _pid = pid;
            _state = process;
            _log = new WorkflowLogAdapter(LogManager.CreateLog("Process" + pid));

            _client.InjectServices(process);

            IEnumerable<IActivity> activities = Compile(process);
            _activities.Clear();
            _activities.AddRange(activities);

            SetupAdvisors(customAdvisors);
        }

        /// <summary>
        /// Loads a process from the directory and initializes the processor
        /// to run the process.
        /// </summary>
        /// <param name="pid">PID of the new process.</param>
        /// <param name="customAdvisors">The advisors to apply to the activities.</param>
        public void LoadProcess(string pid, IEnumerable<IAdvisor> customAdvisors = null)
        {
            if (pid == null) throw new ArgumentNullException("pid");

            Process processEntry = null;
            ProcessState state = null;
            
            TransactionHelper.Scope
            (
                IsolationLevel.ReadCommitted,
                TimeSpan.FromSeconds(30),
                () => 
                {
                    processEntry = _client.Directory.Get(pid) as Process;
                    state =
                        _stateService.GetLatestState(pid) ??
                        _passThroughStateService.GetLatestState(pid);
                    
                }
            );

            if (processEntry == null)
            {
                throw new ArgumentException(string.Format("Process {0} doesn't exist.", pid));
            }

            if (state == null)
            {
                throw new ArgumentException(string.Format("Process {0} has no any state.", pid));
            }

            var wfDefinitionType = Type.GetType(processEntry.Workflow);
            if (wfDefinitionType == null)
            {
                throw new TypeLoadException(string.Format("Cannot load process definition type {0}.", processEntry.Workflow));
            }

            var process = (Workflow.Process)state.Data.To(wfDefinitionType);

            Init(pid, process, customAdvisors);
        }

        /// <summary>
        /// Saves the current process state to the directory.
        /// </summary>
        public void SaveState(bool isKeyState)
        {
            Guid stateId;

            try
            {
                stateId = _stateService.Save(_pid, _state.ToBlob(BlobFormat.GZipXml, true), isKeyState);
            }
            catch (WcfClientException ex)
            {
                Log.Warning(string.Format("Failed to save the state via the state service '{0}'. Saving to the directory.", ex.InnerException.Message));

                stateId = _passThroughStateService.Save(_pid, _state.ToBlob(BlobFormat.GZipXml, true), isKeyState);
            }

            Log.Trace(() => string.Format("State {0} saved.", stateId));
        }

        /// <summary>
        /// Executes the process from the current position until an interruption.
        /// </summary>
        /// <returns>The interruption occured.</returns>
        public Interruption Execute()
        {
            while (!IsFinished)
            {
                try
                {
                    Interruption interruption = NextActivity.Do(this);

                    var unhandledException = interruption as UnhandledException;
                    if (unhandledException != null)
                    {
                        _stateService.MarkLatestStateAsFinalState(_pid);
                    }

                    if (interruption != null)
                    {
                        return interruption;
                    }
                }
                catch (Exception ex)
                {
                    _stateService.MarkLatestStateAsFinalState(_pid);

                    return new UnhandledException { Exception = ex };
                }
            }

            _stateService.MarkLatestStateAsFinalState(_pid);

            return Finish.Instance;
        }

        /// <summary>
        /// Executes the current activity.
        /// </summary>
        /// <returns>
        /// The interruption returned by the activity or Finish if all activities have been done.
        /// </returns>
        public Interruption ExecuteActivity()
        {
            if (!IsFinished)
            {
                try
                {
                    Interruption interruption = NextActivity.Do(this);

                    if (IsFinished)
                    {
                        _stateService.MarkLatestStateAsFinalState(_pid);
                        return Finish.Instance;
                    }

                    var unhandledException = interruption as UnhandledException;
                    if (unhandledException != null)
                    {
                        _stateService.MarkLatestStateAsFinalState(_pid);
                    }

                    return interruption;
                }
                catch (Exception ex)
                {
                    _stateService.MarkLatestStateAsFinalState(_pid);

                    return new UnhandledException { Exception = ex };
                }
            }

            return Finish.Instance;
        }
    }
}
